/*
 * Created on 2009-07-09
 */
package org.jivesoftware.smackx.pubsub;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.OrFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.packet.DelayInformation;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.Header;
import org.jivesoftware.smackx.packet.HeadersExtension;
import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener;
import org.jivesoftware.smackx.pubsub.listener.ItemEventListener;
import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener;
import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;
import org.jivesoftware.smackx.pubsub.util.NodeUtils;

abstract public class Node {
	protected Connection con;
	protected String id;
	protected String to;

	protected ConcurrentHashMap<ItemEventListener, PacketListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener, PacketListener>();
	protected ConcurrentHashMap<ItemDeleteListener, PacketListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, PacketListener>();
	protected ConcurrentHashMap<NodeConfigListener, PacketListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, PacketListener>();

	/**
	 * Construct a node associated to the supplied connection with the specified
	 * node id.
	 * 
	 * @param connection
	 *            The connection the node is associated with
	 * @param nodeName
	 *            The node id
	 */
	Node(Connection connection, String nodeName) {
		con = connection;
		id = nodeName;
	}

	/**
	 * Some XMPP servers may require a specific service to be addressed on the
	 * server.
	 * 
	 * For example, OpenFire requires the server to be prefixed by <b>pubsub</b>
	 */
	public void setTo(String toAddress) {
		to = toAddress;
	}

	/**
	 * Get the NodeId
	 * 
	 * @return the node id
	 */
	public String getId() {
		return id;
	}

	/**
	 * Returns a configuration form, from which you can create an answer form to
	 * be submitted via the {@link #sendConfigurationForm(Form)}.
	 * 
	 * @return the configuration form
	 */
	public ConfigureForm getNodeConfiguration() throws XMPPException {
		Packet reply = sendPubsubPacket(Type.GET, new NodeExtension(
				PubSubElementType.CONFIGURE_OWNER, getId()),
				PubSubNamespace.OWNER);
		return NodeUtils.getFormFromPacket(reply,
				PubSubElementType.CONFIGURE_OWNER);
	}

	/**
	 * Update the configuration with the contents of the new {@link Form}
	 * 
	 * @param submitForm
	 */
	public void sendConfigurationForm(Form submitForm) throws XMPPException {
		PubSub packet = createPubsubPacket(Type.SET, new FormNode(
				FormNodeType.CONFIGURE_OWNER, getId(), submitForm),
				PubSubNamespace.OWNER);
		SyncPacketSend.getReply(con, packet);
	}

	/**
	 * Discover node information in standard {@link DiscoverInfo} format.
	 * 
	 * @return The discovery information about the node.
	 * 
	 * @throws XMPPException
	 */
	public DiscoverInfo discoverInfo() throws XMPPException {
		DiscoverInfo info = new DiscoverInfo();
		info.setTo(to);
		info.setNode(getId());
		return (DiscoverInfo) SyncPacketSend.getReply(con, info);
	}

	/**
	 * Get the subscriptions currently associated with this node.
	 * 
	 * @return List of {@link Subscription}
	 * 
	 * @throws XMPPException
	 */
	public List<Subscription> getSubscriptions() throws XMPPException {
		PubSub reply = (PubSub) sendPubsubPacket(Type.GET, new NodeExtension(
				PubSubElementType.SUBSCRIPTIONS, getId()));
		SubscriptionsExtension subElem = (SubscriptionsExtension) reply
				.getExtension(PubSubElementType.SUBSCRIPTIONS);
		return subElem.getSubscriptions();
	}

	/**
	 * The user subscribes to the node using the supplied jid. The bare jid
	 * portion of this one must match the jid for the connection.
	 * 
	 * Please note that the {@link Subscription.State} should be checked on
	 * return since more actions may be required by the caller.
	 * {@link Subscription.State#pending} - The owner must approve the
	 * subscription request before messages will be received.
	 * {@link Subscription.State#unconfigured} - If the
	 * {@link Subscription#isConfigRequired()} is true, the caller must
	 * configure the subscription before messages will be received. If it is
	 * false the caller can configure it but is not required to do so.
	 * 
	 * @param jid
	 *            The jid to subscribe as.
	 * @return The subscription
	 * @exception XMPPException
	 */
	public Subscription subscribe(String jid) throws XMPPException {
		PubSub reply = (PubSub) sendPubsubPacket(Type.SET,
				new SubscribeExtension(jid, getId()));
		return (Subscription) reply
				.getExtension(PubSubElementType.SUBSCRIPTION);
	}

	/**
	 * The user subscribes to the node using the supplied jid and subscription
	 * options. The bare jid portion of this one must match the jid for the
	 * connection.
	 * 
	 * Please note that the {@link Subscription.State} should be checked on
	 * return since more actions may be required by the caller.
	 * {@link Subscription.State#pending} - The owner must approve the
	 * subscription request before messages will be received.
	 * {@link Subscription.State#unconfigured} - If the
	 * {@link Subscription#isConfigRequired()} is true, the caller must
	 * configure the subscription before messages will be received. If it is
	 * false the caller can configure it but is not required to do so.
	 * 
	 * @param jid
	 *            The jid to subscribe as.
	 * @return The subscription
	 * @exception XMPPException
	 */
	public Subscription subscribe(String jid, SubscribeForm subForm)
			throws XMPPException {
		PubSub request = createPubsubPacket(Type.SET, new SubscribeExtension(
				jid, getId()));
		request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm));
		PubSub reply = (PubSub) PubSubManager.sendPubsubPacket(con, jid,
				Type.SET, request);
		return (Subscription) reply
				.getExtension(PubSubElementType.SUBSCRIPTION);
	}

	/**
	 * Remove the subscription related to the specified JID. This will only work
	 * if there is only 1 subscription. If there are multiple subscriptions, use
	 * {@link #unsubscribe(String, String)}.
	 * 
	 * @param jid
	 *            The JID used to subscribe to the node
	 * 
	 * @throws XMPPException
	 */
	public void unsubscribe(String jid) throws XMPPException {
		unsubscribe(jid, null);
	}

	/**
	 * Remove the specific subscription related to the specified JID.
	 * 
	 * @param jid
	 *            The JID used to subscribe to the node
	 * @param subscriptionId
	 *            The id of the subscription being removed
	 * 
	 * @throws XMPPException
	 */
	public void unsubscribe(String jid, String subscriptionId)
			throws XMPPException {
		sendPubsubPacket(Type.SET, new UnsubscribeExtension(jid, getId(),
				subscriptionId));
	}

	/**
	 * Returns a SubscribeForm for subscriptions, from which you can create an
	 * answer form to be submitted via the {@link #sendConfigurationForm(Form)}.
	 * 
	 * @return A subscription options form
	 * 
	 * @throws XMPPException
	 */
	public SubscribeForm getSubscriptionOptions(String jid)
			throws XMPPException {
		return getSubscriptionOptions(jid, null);
	}

	/**
	 * Get the options for configuring the specified subscription.
	 * 
	 * @param jid
	 *            JID the subscription is registered under
	 * @param subscriptionId
	 *            The subscription id
	 * 
	 * @return The subscription option form
	 * 
	 * @throws XMPPException
	 */
	public SubscribeForm getSubscriptionOptions(String jid,
			String subscriptionId) throws XMPPException {
		PubSub packet = (PubSub) sendPubsubPacket(Type.GET,
				new OptionsExtension(jid, getId(), subscriptionId));
		FormNode ext = (FormNode) packet
				.getExtension(PubSubElementType.OPTIONS);
		return new SubscribeForm(ext.getForm());
	}

	/**
	 * Register a listener for item publication events. This listener will get
	 * called whenever an item is published to this node.
	 * 
	 * @param listener
	 *            The handler for the event
	 */
	public void addItemEventListener(ItemEventListener listener) {
		PacketListener conListener = new ItemEventTranslator(listener);
		itemEventToListenerMap.put(listener, conListener);
		con.addPacketListener(conListener, new EventContentFilter(
				EventElementType.items.toString(), "item"));
	}

	/**
	 * Unregister a listener for publication events.
	 * 
	 * @param listener
	 *            The handler to unregister
	 */
	public void removeItemEventListener(ItemEventListener listener) {
		PacketListener conListener = itemEventToListenerMap.remove(listener);

		if (conListener != null)
			con.removePacketListener(conListener);
	}

	/**
	 * Register a listener for configuration events. This listener will get
	 * called whenever the node's configuration changes.
	 * 
	 * @param listener
	 *            The handler for the event
	 */
	public void addConfigurationListener(NodeConfigListener listener) {
		PacketListener conListener = new NodeConfigTranslator(listener);
		configEventToListenerMap.put(listener, conListener);
		con.addPacketListener(conListener, new EventContentFilter(
				EventElementType.configuration.toString()));
	}

	/**
	 * Unregister a listener for configuration events.
	 * 
	 * @param listener
	 *            The handler to unregister
	 */
	public void removeConfigurationListener(NodeConfigListener listener) {
		PacketListener conListener = configEventToListenerMap.remove(listener);

		if (conListener != null)
			con.removePacketListener(conListener);
	}

	/**
	 * Register an listener for item delete events. This listener gets called
	 * whenever an item is deleted from the node.
	 * 
	 * @param listener
	 *            The handler for the event
	 */
	public void addItemDeleteListener(ItemDeleteListener listener) {
		PacketListener delListener = new ItemDeleteTranslator(listener);
		itemDeleteToListenerMap.put(listener, delListener);
		EventContentFilter deleteItem = new EventContentFilter(
				EventElementType.items.toString(), "retract");
		EventContentFilter purge = new EventContentFilter(
				EventElementType.purge.toString());

		con.addPacketListener(delListener, new OrFilter(deleteItem, purge));
	}

	/**
	 * Unregister a listener for item delete events.
	 * 
	 * @param listener
	 *            The handler to unregister
	 */
	public void removeItemDeleteListener(ItemDeleteListener listener) {
		PacketListener conListener = itemDeleteToListenerMap.remove(listener);

		if (conListener != null)
			con.removePacketListener(conListener);
	}

	@Override
	public String toString() {
		return super.toString() + " " + getClass().getName() + " id: " + id;
	}

	protected PubSub createPubsubPacket(Type type, PacketExtension ext) {
		return createPubsubPacket(type, ext, null);
	}

	protected PubSub createPubsubPacket(Type type, PacketExtension ext,
			PubSubNamespace ns) {
		return PubSubManager.createPubsubPacket(to, type, ext, ns);
	}

	protected Packet sendPubsubPacket(Type type, NodeExtension ext)
			throws XMPPException {
		return PubSubManager.sendPubsubPacket(con, to, type, ext);
	}

	protected Packet sendPubsubPacket(Type type, NodeExtension ext,
			PubSubNamespace ns) throws XMPPException {
		return PubSubManager.sendPubsubPacket(con, to, type, ext, ns);
	}

	private static List<String> getSubscriptionIds(Packet packet) {
		HeadersExtension headers = (HeadersExtension) packet.getExtension(
				"headers", "http://jabber.org/protocol/shim");
		List<String> values = null;

		if (headers != null) {
			values = new ArrayList<String>(headers.getHeaders().size());

			for (Header header : headers.getHeaders()) {
				values.add(header.getValue());
			}
		}
		return values;
	}

	/**
	 * This class translates low level item publication events into api level
	 * objects for user consumption.
	 * 
	 * @author Robin Collier
	 */
	public class ItemEventTranslator implements PacketListener {
		private ItemEventListener listener;

		public ItemEventTranslator(ItemEventListener eventListener) {
			listener = eventListener;
		}

		public void processPacket(Packet packet) {
			EventElement event = (EventElement) packet.getExtension("event",
					PubSubNamespace.EVENT.getXmlns());
			ItemsExtension itemsElem = (ItemsExtension) event.getEvent();
			DelayInformation delay = (DelayInformation) packet.getExtension(
					"delay", "urn:xmpp:delay");

			// If there was no delay based on XEP-0203, then try XEP-0091 for
			// backward compatibility
			if (delay == null) {
				delay = (DelayInformation) packet.getExtension("x",
						"jabber:x:delay");
			}
			ItemPublishEvent eventItems = new ItemPublishEvent(
					itemsElem.getNode(), (List<Item>) itemsElem.getItems(),
					getSubscriptionIds(packet), (delay == null ? null
							: delay.getStamp()));
			listener.handlePublishedItems(eventItems);
		}
	}

	/**
	 * This class translates low level item deletion events into api level
	 * objects for user consumption.
	 * 
	 * @author Robin Collier
	 */
	public class ItemDeleteTranslator implements PacketListener {
		private ItemDeleteListener listener;

		public ItemDeleteTranslator(ItemDeleteListener eventListener) {
			listener = eventListener;
		}

		public void processPacket(Packet packet) {
			EventElement event = (EventElement) packet.getExtension("event",
					PubSubNamespace.EVENT.getXmlns());

			List<PacketExtension> extList = event.getExtensions();

			if (extList.get(0).getElementName()
					.equals(PubSubElementType.PURGE_EVENT.getElementName())) {
				listener.handlePurge();
			} else {
				ItemsExtension itemsElem = (ItemsExtension) event.getEvent();
				Collection<? extends PacketExtension> pubItems = itemsElem
						.getItems();
				Iterator<RetractItem> it = (Iterator<RetractItem>) pubItems
						.iterator();
				List<String> items = new ArrayList<String>(pubItems.size());

				while (it.hasNext()) {
					RetractItem item = it.next();
					items.add(item.getId());
				}

				ItemDeleteEvent eventItems = new ItemDeleteEvent(
						itemsElem.getNode(), items, getSubscriptionIds(packet));
				listener.handleDeletedItems(eventItems);
			}
		}
	}

	/**
	 * This class translates low level node configuration events into api level
	 * objects for user consumption.
	 * 
	 * @author Robin Collier
	 */
	public class NodeConfigTranslator implements PacketListener {
		private NodeConfigListener listener;

		public NodeConfigTranslator(NodeConfigListener eventListener) {
			listener = eventListener;
		}

		public void processPacket(Packet packet) {
			EventElement event = (EventElement) packet.getExtension("event",
					PubSubNamespace.EVENT.getXmlns());
			ConfigurationEvent config = (ConfigurationEvent) event.getEvent();

			listener.handleNodeConfiguration(config);
		}
	}

	/**
	 * Filter for {@link PacketListener} to filter out events not specific to
	 * the event type expected for this node.
	 * 
	 * @author Robin Collier
	 */
	class EventContentFilter implements PacketFilter {
		private String firstElement;
		private String secondElement;

		EventContentFilter(String elementName) {
			firstElement = elementName;
		}

		EventContentFilter(String firstLevelEelement, String secondLevelElement) {
			firstElement = firstLevelEelement;
			secondElement = secondLevelElement;
		}

		public boolean accept(Packet packet) {
			if (!(packet instanceof Message))
				return false;

			EventElement event = (EventElement) packet.getExtension("event",
					PubSubNamespace.EVENT.getXmlns());

			if (event == null)
				return false;

			NodeExtension embedEvent = event.getEvent();

			if (embedEvent == null)
				return false;

			if (embedEvent.getElementName().equals(firstElement)) {
				if (!embedEvent.getNode().equals(getId()))
					return false;

				if (secondElement == null)
					return true;

				if (embedEvent instanceof EmbeddedPacketExtension) {
					List<PacketExtension> secondLevelList = ((EmbeddedPacketExtension) embedEvent)
							.getExtensions();

					if (secondLevelList.size() > 0
							&& secondLevelList.get(0).getElementName()
									.equals(secondElement))
						return true;
				}
			}
			return false;
		}
	}
}
